package minmax;

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

/**
 * wraps over a game state and tracks all the actions. Whenever underlying game state
 * fails with exception or violates constrains, this wrap throws an exception which
 * contains the history log, using which the error condition can be reproduced.
 * 
 * @author alex
 *
 * @param <M> is type of moves
 */
public class GameSentinel<M extends GameMove> implements GameState<M> {

	/**
	 * game state to wrap over
	 */
	private GameState<M> state;
	
	/**
	 * copy of the initial state
	 */
	private GameState<M> top;
	
	/**
	 * indicates whether sentinel stores the full moves history
	 */
	private boolean verbose;
	
	/**
	 * verifying sentinel keeps copies of states and checks the
	 * consistency of move/backtrack actions
	 */
	private boolean verify;
	
	/**
	 * stack of moves
	 */
	private List<M> moves;
	
	/**
	 * full list of moves, <code>null</code> means a backtrack action
	 */
	private List<M> history;
	
	/**
	 * stack of state copies to compare with
	 */
	private List<GameState<M>> states;
	
	/**
	 * current player
	 */
	private boolean player;
	
	/**
	 * error description, if any
	 */
	private ErrorCase<M> description;
	
	/**
	 * creates a sentinel wrap
	 * 
	 * @param state is a game state to keep an eye on
	 * @param verbose indicates whether sentinel stores the full moves history
	 * @param verify indicates whether this sentinel checks consistency of
	 *  move/backtrack actions (that backtrack leads to a previos state)
	 */
	public GameSentinel(GameState<M> state, boolean verbose, boolean verify) {
		if (state == null) {
			throw new NullPointerException("state is required");
		}
		this.state = state;
		this.verbose = verbose;
		this.verify = verify;
		this.player = state.player();
		// create watches
		this.moves = new ArrayList<M>();
		if (verbose) {
			this.history = new ArrayList<M>();
		}
		if (verify) {
			this.states = new ArrayList<GameState<M>>();
			this.states.add(state.clone());
		}
	}
	
	/**
	 * @return game state
	 */
	private GameState<M> state() {
		if (state == null) {
			throw new IllegalStateException("the sentinel has already been failed");
		}
		return state;
	}
	
	/**
	 * keeps history of moves, called after a move/backtrack action is performed
	 * 
	 * @param move is a move or <code>null</code> to indicate a backtrack
	 */
	private void log (M move) {
		// keep the stack of moves
		if (move == null) {
			assert moves.isEmpty() : "a backtrack to be logged must be already applied";
			moves.remove(moves.size() - 1);
		} else {
			moves.add(move);
		}
		// keep full history
		if (verbose) {
			history.add(move);
		}
		// keep states history
		if (verify) {
			// states stack is synchronized with moves stack
			if (move == null) {
				states.remove(states.size() - 1);
			} else {
				states.add(state().clone());
			}
		}
		// keep current player updated
		player = !player;
	}
	
	/**
	 * throws exception that contains description of the error
	 * 
	 * @param type is source of error
	 * @param e is the exception, thrown by the inspected state
	 * @throws SentinelException is the report exception being created
	 */
	private void fail(FailureType type, Exception e, GameState<M> validState) throws SentinelException {
		assert (type != FailureType.MANUAL_FAILURE) || (e == null && validState == null);
		assert e == null || validState == null;
		if (e == null) {
			this.description = new ErrorCase<M>(top, state(), moves, history, type);
		} else {
			this.description = new ErrorCase<M>(top, state(), moves, history, e, type);
		}
		this.state = null;
		throw new SentinelException(description);
	}
	
	/**
	 * throws exception that contains description of the error
	 * 
	 * @param e is the exception, thrown by the inspected state
	 * @param movesIndex is index of the move associated with the iterator
	 * @param historyIndex is index of the move associated with the iterator
	 * @throws SentinelException is the report exception being created
	 */
	private void fail(Exception e, int movesIndex, int historyIndex) throws SentinelException {
		this.description = new ErrorCase<M>(top, state(), moves, history, e, movesIndex, historyIndex);
		this.state = null;
		throw new SentinelException(description);
	}
	
	/**
	 * marks current state as failed
	 * 
	 * @throws SentinelException is the report exception
	 */
	public void fail() throws SentinelException {
		this.description = new ErrorCase<M>(top, state(), moves, history, FailureType.MANUAL_FAILURE);
		this.state = null;
		throw new SentinelException(description);
	}
	
	/**
	 * resets moves and history
	 */
	public void clear() {
		top = state().clone();
		moves.clear();
		if (verbose) {
			history.clear();
		}
		if (verify) {
			states.clear();
			states.add(top);
		}
	}
	
	/**
	 * retrieves error description (after an exception thrown)
	 * 
	 * @return description
	 */
	public ErrorCase<M> description() {
		if (description == null) {
			throw new IllegalStateException("no errors have occured so far");
		}
		return description;
	}
	
	/**
	 * delegates a move and produces errors
	 * 
	 * @see minmax.GameState#move(minmax.GameMove)
	 */
	@Override
	public void move(M step) {
		if (step == null) {
			throw new NullPointerException();
		}
		// delegate to move method
		try {
			try {
				state().move(step);
			} finally {
				log(step);
			}
		} catch (Exception e) {
			fail(FailureType.ACTION_FAILURE, e, null);
		}
	}

	/**
	 * delegates a backtrack and produces errors
	 * 
	 * @see minmax.GameState#backtrack()
	 */
	@Override
	public void backtrack() {
		if (moves.isEmpty()) {
			throw new IllegalStateException("number of backtracks is greater then number of moves");
		}
		// delegate to backtrack method
		try {
			try {
				state().backtrack();
			} finally {
				log(null);
			}
		} catch (Exception e) {
			fail(FailureType.ACTION_FAILURE, e, null);
		}
		// verify the state
		if (verify) {
			GameState<M> top = states.get(states.size() - 1); 
			if (!state().equals(top)) {
				fail(FailureType.ACTION_FAILURE, null, top);
			}
		}
		
	}
	
	/**
	 * creates checking iterator
	 * 
	 * @see minmax.GameState#moves()
	 */
	@Override
	public Iterator<M> moves() {
		Iterator<M> moves = null;
		try {
			moves = state().moves();
		} catch (Exception e) {
			fail(FailureType.PROPERTY_FAILURE, e, null);
		}
		if (moves == null) {
			fail(FailureType.PROPERTY_FAILURE, null, null);
		}
		final Iterator<M> iterator = moves;
		return new Iterator<M>() {

			private Iterator<M> i = iterator;
			
			private int movesIndex = GameSentinel.this.moves.size();
			
			private int historyIndex = verbose ? history.size() : -1;
			
			@Override
			public boolean hasNext() {
				boolean result = false;
				try {
					result = i.hasNext();
				} catch (Exception e) {
					fail(e, movesIndex, historyIndex);
				}
				return result;
			}

			@Override
			public M next() {
				M result = null;
				try {
					if (i.hasNext()) {
						result = i.next();
					} else {
						throw new IllegalStateException("hasNext() returns false");
					}
				} catch(Exception e) {
					fail(e, movesIndex, historyIndex);
				}
				if (result == null) {
					fail(null, movesIndex, historyIndex);
				}
				return result;
			}

			@Override
			public void remove() {
				throw new UnsupportedOperationException("iterator is read-only");
			}
			
		};
	}

	/**
	 * checks via iteration over valid moves
	 * 
	 * @see minmax.GameState#canMove(minmax.GameMove)
	 */
	@Override
	public boolean canMove(M move) {
		if (move == null) {
			throw new NullPointerException();
		}
		boolean result = false;
		try {
			// determine the result
			Iterator<M> i = state().moves();
			while (i.hasNext()) {
				if (move.equals(i.next())) {
					result = true;
					break;
				}
			}
			// compare with original result
			if (state().canMove(move) != result) {
				fail(FailureType.PROPERTY_FAILURE, null, null);
			}
		} catch (SentinelException e) {
			throw e;
		} catch (Exception e) {
			fail(FailureType.PROPERTY_FAILURE, e, null);
		}
		return result;
	}

	/**
	 * check player constraint
	 * 
	 * @see minmax.GameState#player()
	 */
	@Override
	public boolean player() {
		boolean result = false;
		try {
			result = state().player();
		} catch (Exception e) {
			fail(FailureType.PROPERTY_FAILURE, e, null);
		}
		if (player != result) {
			fail(FailureType.PROPERTY_FAILURE, null, null);
		}
		return result;
	}

	/**
	 * checks evaluation constraints
	 * 
	 * @see minmax.GameState#evaluate(boolean)
	 */
	@Override
	public int evaluate(boolean maximize) {
		final int MIN = Integer.MIN_VALUE, MAX = Integer.MAX_VALUE; 
		int min = 0, max = 0;
		try {
			min = state().evaluate(false);
			max = state().evaluate(true);
		} catch (Exception e) {
			fail(FailureType.PROPERTY_FAILURE, e, null);
		}
		if (min == MIN  || min == MAX || max == MIN || max == MAX) {
			fail(FailureType.PROPERTY_FAILURE, null, null);
		}
		return maximize ? max : min;
	}

	/**
	 * checks outcome and finishing conditions
	 * 
	 * @see minmax.GameState#isFinished()
	 */
	@Override
	public boolean isFinished() {
		boolean isFinished = false;
		GameOutcome outcome = null;
		try {
			isFinished = state().isFinished();
			if (isFinished) {
				outcome = state().outcome();
			}
		} catch (Exception e) {
			fail(FailureType.PROPERTY_FAILURE, e, null);
		}
		if (isFinished && outcome == null) {
			fail(FailureType.PROPERTY_FAILURE, null, null);
		}
		return isFinished;
	}

	/**
	 * checks outcome and finishing conditions
	 * 
	 * @see minmax.GameState#outcome()
	 */
	@Override
	public GameOutcome outcome() {
		boolean isFinished = false;
		GameOutcome outcome = null;
		try {
			isFinished = state().isFinished();
			if (isFinished) {
				outcome = state().outcome();
			}
		} catch (Exception e) {
			fail(FailureType.PROPERTY_FAILURE, e, null);
		}
		if (!isFinished) {
			throw new IllegalStateException("game is not finished");
		}
		if (outcome == null) {
			fail(FailureType.PROPERTY_FAILURE, null, null);
		}
		return outcome;
	}

	/**
	 * cloned sentinel is like a sentinel after reset
	 * 
	 * @see java.lang.Object#clone()
	 */
	@Override
	public GameSentinel<M> clone() {
		return new GameSentinel<M>(state(), verbose, verify);
	}

	/**
	 * displays most relevant information
	 * 
	 * @see java.lang.Object#toString()
	 */
	@Override
	public String toString() {
		String result = "state : " + state + ", verbose : " + verbose + ", verify : " + verify;
		result += "moves : " + moves;
		return result;
	}

	/**
	 * Type of failure
	 */
	public enum FailureType {
		MANUAL_FAILURE, ACTION_FAILURE, MOVES_FAILURE, PROPERTY_FAILURE;
	}
	
	/**
	 * records that describes failure in details
	 *
	 * @param <M> is type of moves
	 */
	public static class ErrorCase<M extends GameMove> {
		
		/**
		 * defines type of failure
		 */
		public final FailureType type;
		
		/**
		 * an optional exception, if error condition is caused by an exception
		 */
		public final Exception cause;
		
		/**
		 * a top state from which we can move to any other described state
		 */
		public final GameState<M> initialState;
		
		/**
		 * optional valid state (encountered is move/backtrack actions are verified)
		 */
		public final GameState<M> validState;
		
		/**
		 * the state at which an error has been found
		 */
		public final GameState<M> invalidState;
		
		/**
		 * shortest path from initial state to invalid state
		 */
		public final List<M> moves;
		
		/**
		 * optional full history of moves
		 */
		public final List<M> history;
		
		/**
		 * iterator position in moves, or </code>-1</code>
		 */
		public final int movesIndex;
		
		/**
		 * iterator position in history, or </code>-1</code>
		 */
		public final int historyIndex;
		
		/**
		 * creates manual failure case description
		 * 
		 * @param initialState is the initial state
		 * @param invalidState is the invalid state
		 * @param moves is list of moves
		 * @param history is optional full history
		 * @param type is either a manual failure or a property failure
		 */
		public ErrorCase(GameState<M> initialState, GameState<M> invalidState, List<M> moves, List<M> history, FailureType type) {
			if (initialState == null || invalidState == null || moves == null) {
				throw new NullPointerException();
			}
			if (type != FailureType.MANUAL_FAILURE && type != FailureType.PROPERTY_FAILURE) {
				throw new IllegalArgumentException("invalid type : " + type);
			}
			this.type = type;
			this.cause = null;
			this.initialState = initialState;
			this.validState = null;
			this.invalidState = invalidState;
			this.moves = moves;
			this.history = history;
			this.movesIndex = -1;
			this.historyIndex = -1;
		}
		
		/**
		 * creates action sequence failure case
		 * 
		 * @param initialState is the initial state
		 * @param invalidState is the invalid state
		 * @param moves is list of moves
		 * @param history is optional full history
		 * @param validState is the state the expected state
		 */
		public ErrorCase(GameState<M> initialState, GameState<M> invalidState, List<M> moves, List<M> history, GameState<M> validState) {
			if (initialState == null || invalidState == null || moves == null || validState == null) {
				throw new NullPointerException();
			}
			this.type = FailureType.ACTION_FAILURE;
			this.cause = null;
			this.initialState = initialState;
			this.validState = validState;
			this.invalidState = invalidState;
			this.moves = moves;
			this.history = history;
			this.movesIndex = -1;
			this.historyIndex = -1;
		}
		
		/**
		 * creates action sequence failure case
		 * 
		 * @param initialState is the initial state
		 * @param invalidState is the invalid state
		 * @param moves is list of moves
		 * @param history is optional full history
		 * @param cause is the exception
		 * @param type is the failure type
		 */
		public ErrorCase(GameState<M> initialState, GameState<M> invalidState, List<M> moves, List<M> history, Exception cause, FailureType type) {
			if (initialState == null || invalidState == null || moves == null || cause == null || type == null) {
				throw new NullPointerException();
			}
			this.type = type;
			this.cause = cause;
			this.initialState = initialState;
			this.validState = null;
			this.invalidState = invalidState;
			this.moves = moves;
			this.history = history;
			this.movesIndex = -1;
			this.historyIndex = -1;
		}
		
		/**
		 * creates action sequence failure case
		 * 
		 * @param initialState is the initial state
		 * @param invalidState is the invalid state
		 * @param moves is list of moves
		 * @param history is optional full history
		 * @param cause is the exception
		 * @param movesIndex is index of the move associated with the iterator
		 * @param historyIndex is index of the move associated with the iterator
		 */
		public ErrorCase(GameState<M> initialState, GameState<M> invalidState, List<M> moves, List<M> history, Exception cause, 
				int movesIndex, int historyIndex) {
			if (initialState == null || invalidState == null || moves == null) {
				throw new NullPointerException();
			}
			if (movesIndex < 0 || historyIndex < -1 || (historyIndex == -1 && history != null) || (history == null && historyIndex != -1)) {
				throw new IllegalArgumentException("invalid indices : " + movesIndex + ", " + historyIndex);
			}
			this.type = FailureType.MOVES_FAILURE;
			this.cause = cause;
			this.initialState = initialState;
			this.validState = null;
			this.invalidState = invalidState;
			this.moves = moves;
			this.history = history;
			this.movesIndex = -1;
			this.historyIndex = -1;
		}

		/**
		 * returns the most relevant part of state
		 * 
		 * @see java.lang.Object#toString()
		 */
		@Override
		public String toString() {
			String result = "type : " + type + ", invalid state : " + invalidState;
			if (cause != null) {
				result += ", cause : " + cause;
			}
			result += ", moves : " + moves;
			return result;
		}
		
	}
	
	/**
	 * describes a error condition in a game state
	 */
	public static class SentinelException extends RuntimeException {
		
		/**
		 * serial version
		 */
		private static final long serialVersionUID = 768986098147481563L;
		
		/**
		 * creates sentinel exception
		 * 
		 * @param description is error description
		 */
		public SentinelException(ErrorCase<? extends GameMove> description) {
			if (description.cause != null) {
				initCause(description.cause);
			}
		}

		
		
	}
	
}
